/*
 * Copyright (c) 2024 AlgoIdeas <yu19881234@163.com>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <endian.h>

#include "NTPClient.h"

#define JAN_1970            0x83aa7e80

#define NTP_VERSION_3           3
#define NTP_VERSION_4           4
 
#define NTP_MODE_CLIENT         3
#define NTP_MODE_SERVER         4
 
#define NTP_LI              0
#define NTP_VN              NTP_VERSION_3   
#define NTP_MODE            NTP_MODE_CLIENT
#define NTP_STRATUM         0
#define NTP_POLL            4
#define NTP_PRECISION       -6
 
#define NTP_HLEN            48
 
#define TIMEOUT             10
 
#define BUFSIZE             1500

#define NTP_CONV_FRAC32(x)  (uint64_t) ((x) * ((uint64_t)1<<32))    
#define NTP_REVE_FRAC32(x)  ((double) ((double) (x) / ((uint64_t)1<<32)))   

#define NTP_CONV_FRAC16(x)  (uint32_t) ((x) * ((uint32_t)1<<16))    
#define NTP_REVE_FRAC16(x)  ((double)((double) (x) / ((uint32_t)1<<16)))    

#define USEC2FRAC(x)        ((uint32_t) NTP_CONV_FRAC32( (x) / 1000000.0 )) 
#define FRAC2USEC(x)        ((uint32_t) NTP_REVE_FRAC32( (x) * 1000000.0 )) 

#define NTP_LFIXED2DOUBLE(x)    ((double) ( ntohl(((struct l_fixedpt *) (x))->intpart) - JAN_1970 + FRAC2USEC(ntohl(((struct l_fixedpt *) (x))->fracpart)) / 1000000.0 ))   

struct s_fixedpt {
    uint16_t    intpart;
    uint16_t    fracpart;
};
 
struct l_fixedpt {
    uint32_t    intpart;
    uint32_t    fracpart;
};

struct ntphdr {
    uint32_t    ntp_mode:3;
    uint32_t    ntp_vn:3;
    uint32_t    ntp_li:2;
    uint8_t         ntp_stratum;
    uint8_t         ntp_poll;
    int8_t          ntp_precision;
    struct s_fixedpt    ntp_rtdelay;
    struct s_fixedpt    ntp_rtdispersion;
    uint32_t            ntp_refid;
    struct l_fixedpt    ntp_refts;
    struct l_fixedpt    ntp_orits;
    struct l_fixedpt    ntp_recvts;
    struct l_fixedpt    ntp_transts;
};

static in_addr_t Inet2Host(const char *host)
{
    in_addr_t saddr;
    struct hostent *hostent;
 
    if ((saddr = inet_addr(host)) == INADDR_NONE) {
        if ((hostent = gethostbyname(host)) == NULL)
            return INADDR_NONE;
 
        memmove(&saddr, hostent->h_addr, hostent->h_length);
    }
 
    return saddr;
}

static int32_t GetNTPPacket(void *buf, size_t *size)
{
    struct ntphdr *ntp;
    struct timeval tv;

    if (!size || *size<NTP_HLEN)
        return NTP_ERR;
 
    memset(buf, 0, *size);
 
    ntp = (struct ntphdr *) buf;
    ntp->ntp_li = NTP_LI;
    ntp->ntp_vn = NTP_VN;
    ntp->ntp_mode = NTP_MODE;
    ntp->ntp_stratum = NTP_STRATUM;
    ntp->ntp_poll = NTP_POLL;
    ntp->ntp_precision = NTP_PRECISION;
 
    gettimeofday(&tv, NULL);
    ntp->ntp_transts.intpart = htonl(tv.tv_sec + JAN_1970);
    ntp->ntp_transts.fracpart = htonl(USEC2FRAC(tv.tv_usec));
 
    *size = NTP_HLEN;
 
    return 0;
}

static double GetOffset(const struct ntphdr *ntp, const struct timeval *recvtv)  //偏移量
{
    double t1, t2, t3, t4;
 
    t1 = NTP_LFIXED2DOUBLE(&ntp->ntp_orits);
    t2 = NTP_LFIXED2DOUBLE(&ntp->ntp_recvts);
    t3 = NTP_LFIXED2DOUBLE(&ntp->ntp_transts);
    t4 = recvtv->tv_sec + recvtv->tv_usec / 1000000.0;
 
    return ((t2 - t1) + (t3 - t4)) / 2;
}

int32_t NTPClientConnect(NTPClient *client)
{
    int32_t sockfd = 0;

    if (client == NULL) {
        osal_printk("client is null!");
        return NTP_ERR;
    }

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(client->serverPort);
    servaddr.sin_addr.s_addr = Inet2Host(client->serverIP);

    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        osal_printk("socket error\n");
        return NTP_ERR;
    }

    if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(struct sockaddr)) != 0) {
        osal_printk("connect error\n");
        return NTP_ERR;
    }

    client->clientId = sockfd;

    return NTP_OK;
}

int64_t NTPClientSync(NTPClient *client)
{
    char dateBuf[64] = {0};
    char cmd[128] = {0};
    char buf[BUFSIZE];
    size_t nbytes;
    int32_t maxfd1;
    struct tm* tm_s;
    int32_t sockfd = 0;

    fd_set readfds;
    struct timeval timeout, recvtv, tv;
    double offset;
    int64_t time = NTP_ERR;

    if (client == NULL) {
        osal_printk("client is null!\n");
        return NTP_ERR;
    }

    sockfd = client->clientId;

    nbytes = BUFSIZE;
    if (GetNTPPacket(buf, &nbytes) != 0) {
        osal_printk("construct ntp request error \n");
        return NTP_ERR;
    }
    send(sockfd, buf, nbytes, 0);

    FD_ZERO(&readfds);
    FD_SET(sockfd, &readfds);
    maxfd1 = sockfd + 1;

    timeout.tv_sec = TIMEOUT;
    timeout.tv_usec = 0;

    if (select(maxfd1, &readfds, NULL, NULL, &timeout) > 0) {
        if (FD_ISSET(sockfd, &readfds)) {
            nbytes = recv(sockfd, buf, BUFSIZE, 0);
            if (nbytes <= 0) {
                osal_printk("recv error\n");
                return NTP_ERR;
            }

            gettimeofday(&recvtv, NULL);
            offset = GetOffset((struct ntphdr *) buf, &recvtv);
            gettimeofday(&tv, NULL);

            tv.tv_sec += (int) offset + 28800; /* 默认东8区 */
            tv.tv_usec += offset - (int) offset;

            time = tv.tv_sec;

            settimeofday(&tv, (void *)0);
        }
    }else {
        osal_printk("Ntp sync timeout.\r\n");
    }

    return time;
}

int32_t NTPClientDisconnect(NTPClient *client)
{
    if (client == NULL) {
        osal_printk("client is null!\n");
        return NTP_ERR;
    }

    close(client->clientId);
    return NTP_OK;
}